package com.google.code.jtasks.bugzilla.proxy.bug;

import java.io.Serializable;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.client.XmlRpcClient;

import com.google.code.jtasks.bugzilla.proxy.ProcessingUtils;
import com.google.code.jtasks.bugzilla.proxy.bug.BugSearchProxy.Bug;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;

/**
 * This class provides a proxy for the <code>Bug.comments</code> XML-RPC method.
 */
public final class BugCommentsProxy
{
	private final XmlRpcClient rpcClient;

	/**
	 * Constructs a new {@link BugCommentsProxy} instance.
	 * 
	 * @param rpcClient
	 *            the {@link XmlRpcClient} that should be used
	 */
	public BugCommentsProxy(XmlRpcClient rpcClient)
	{
		this.rpcClient = rpcClient;
	}

	/**
	 * This method is a proxy for the <code>Bug.comments</code> XML-RPC method.
	 * 
	 * @param params
	 *            the {@link CommentsSelector} that specifies which comments should be returned
	 * @return a {@link CommentsResult} object containing the results of the search
	 */
	public CommentsResult comments(CommentsSelector params)
	{
		Object rpcResultStruct;
		try
		{
			rpcResultStruct = rpcClient.execute("Bug.comments", new Object[] { params.convertToRpcStruct() });
		}
		catch (XmlRpcException e)
		{
			// TODO throw a better exception
			throw new RuntimeException(e);
		}

		return new CommentsResult(rpcResultStruct);
	}

	/**
	 * Specifies the comments that should be returned. Represents the parameters that can be passed to the
	 * <code>Bug.comments</code> XML-RPC method.
	 */
	public static final class CommentsSelector
	{
		private final Set<Integer> ids;

		/**
		 * Constructs a new {@link CommentsSelector} instance.
		 */
		public CommentsSelector()
		{
			this.ids = new HashSet<Integer>();
		}

		/**
		 * Filters the search results to bugs with the specified ID.
		 * 
		 * @param ids
		 *            the IDs of the bugs to include in the search results
		 */
		public void filterByIds(Collection<Integer> ids)
		{
			this.ids.clear();
			this.ids.addAll(ids);
		}

		/**
		 * Converts this {@link CommentsSelector} instance to a "raw" struct that can be passed to an
		 * {@link XmlRpcClient}.
		 * 
		 * @return a "raw" struct that can be passed to an {@link XmlRpcClient}
		 */
		Map<String, Object> convertToRpcStruct()
		{
			Map<String, Object> rpcStruct = new HashMap<String, Object>();

			rpcStruct.put("ids", ProcessingUtils.toIntegerArray(ids));

			return rpcStruct;
		}
	}

	/**
	 * Represents the {@link Comment}s returned by the <code>Bug.comments</code> XML-RPC method.
	 */
	public static final class CommentsResult
	{
		/**
		 * A {@link Map} of {@link Comment}s, grouped by {@link Bug#getId()}.
		 */
		private final Multimap<Integer, Comment> comments;

		/**
		 * Constructs a new {@link CommentsResult} instance from the specified
		 * {@link XmlRpcClient#execute(String, Object[])} result object.
		 * 
		 * @param rpcResultStruct
		 *            the {@link XmlRpcClient#execute(String, Object[])} result object to build the
		 *            {@link CommentsResult} from
		 */
		public CommentsResult(Object rpcResultStruct)
		{
			Map<?, ?> rootMap = ProcessingUtils.castToMap(rpcResultStruct);
			Map<?, ?> bugs = ProcessingUtils.extractMapField(rootMap, "bugs");

			// Walk through each bug in the map, extracting the comments in it
			this.comments = ArrayListMultimap.create();
			for (Entry<?, ?> bugsMapEntry : bugs.entrySet())
			{
				Integer bugId = ProcessingUtils.getStringKeyAsInteger(bugsMapEntry);
				Map<?, ?> bugMap = ProcessingUtils.getValueAsMap(bugsMapEntry);
				Object[] comments = ProcessingUtils.extractObjectArrayField(bugMap, "comments", false);

				// Walk through each comment for the current bug, adding it to the list
				for (Object commentStruct : comments)
				{
					Comment comment = new Comment(commentStruct);
					this.comments.put(bugId, comment);
				}
			}
		}

		/**
		 * Returns a {@link Multimap} of the {@link Comment}s contained in this {@link CommentsResult}, where
		 * each {@link Comment}'s {@link Comment#getBug_id()} is used as the map key.
		 * 
		 * @return a {@link Multimap} of the {@link Comment}s contained in this {@link CommentsResult}, where
		 *         each {@link Comment}'s {@link Comment#getBug_id()} is used as the map key
		 */
		public Multimap<Integer, Comment> getComments()
		{
			return comments;
		}
	}

	/**
	 * Represents a Bugzilla {@link Bug} comment, as returned by the <code>Bug.comments</code> XML-RPC method.
	 */
	public static final class Comment implements Serializable
	{
		private static final long serialVersionUID = 1360173026811999549L;

		private final Integer id;
		private final Integer bug_id;
		private final Integer attachment_id;
		private final String text;
		private final String creator;
		private final Date time;
		private final Boolean is_private;

		/**
		 * Constructs a new {@link CommentsResult} instance from the specified
		 * {@link XmlRpcClient#execute(String, Object[])} result inner object.
		 * 
		 * @param commentStruct
		 *            the {@link XmlRpcClient#execute(String, Object[])} result inner object to build the
		 *            {@link Comment} from
		 */
		public Comment(Object commentStruct)
		{
			Map<?, ?> fieldMap = ProcessingUtils.castToMap(commentStruct);

			this.id = ProcessingUtils.extractIntegerField(fieldMap, "id", false);
			this.bug_id = ProcessingUtils.extractIntegerField(fieldMap, "bug_id", false);
			this.attachment_id = ProcessingUtils.extractIntegerField(fieldMap, "attachment_id", true);
			this.text = ProcessingUtils.extractStringField(fieldMap, "text", false);
			this.creator = ProcessingUtils.extractStringField(fieldMap, "creator", false);
			this.time = ProcessingUtils.extractDateField(fieldMap, "time");
			this.is_private = ProcessingUtils.extractBooleanField(fieldMap, "is_private", false);
		}

		/**
		 * Returns the {@link Bug#getId()} of the {@link Bug} that this {@link Comment} is associated with.
		 * 
		 * @return the {@link Bug#getId()} of the {@link Bug} that this {@link Comment} is associated with
		 */
		public Integer getBug_id()
		{
			return bug_id;
		}

		/**
		 * Returns the value of this field, as returned by the RPC call.
		 * 
		 * @return the value of this field, as returned by the RPC call
		 * @see http://www.bugzilla.org/docs/4.0/en/html/api/Bugzilla/WebService/Bug.html#creator
		 */
		public String getCreator()
		{
			return creator;
		}

		/**
		 * Returns the value of this field, as returned by the RPC call.
		 * 
		 * @return the value of this field, as returned by the RPC call
		 * @see http://www.bugzilla.org/docs/4.0/en/html/api/Bugzilla/WebService/Bug.html#time
		 */
		public Date getTime()
		{
			return time;
		}

		/**
		 * Returns the value of this field, as returned by the RPC call.
		 * 
		 * @return the value of this field, as returned by the RPC call
		 * @see http://www.bugzilla.org/docs/4.0/en/html/api/Bugzilla/WebService/Bug.html#text
		 */
		public String getText()
		{
			return text;
		}
	}
}
